
"""
Fama-French 5-Factor Model API
FastAPI 0.110 + Pandas 2.1 + Statsmodels 0.14
Author: Inscode AI
"""
import io
import os
from typing import List

import pandas as pd
import statsmodels.api as sm
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

app = FastAPI(title="FF5 API", version="0.1.0")

# CORS 允许前端开发端口
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 全局缓存：月度因子数据（Ken French 数据站）
FACTOR_URL = (
    "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/"
    "F-F_Research_Data_5_Factors_2x3_CSV.zip"
)
FACTOR_PATH = "data/ff5.csv"


def load_factors() -> pd.DataFrame:
    """加载或下载 FF5 月度因子数据"""
    os.makedirs("data", exist_ok=True)
    if not os.path.exists(FACTOR_PATH):
        df = pd.read_csv(FACTOR_URL, compression="zip", skiprows=3)
        df.to_csv(FACTOR_PATH, index=False)
    else:
        df = pd.read_csv(FACTOR_PATH)
    df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m")
    df.set_index("date", inplace=True)
    df = df / 100  # 转为小数
    return df


FACTORS = load_factors()


class FactorExposure(BaseModel):
    alpha: float
    beta_mkt: float
    beta_smb: float
    beta_hml: float
    beta_rmw: float
    beta_cma: float
    r_squared: float
    std_resid: float  # 残差标准差，用于 VaR


class Forecast(BaseModel):
    next_month_return: float
    var_95: float  # 1 个月 95% VaR（历史模拟法）


@app.post("/upload", response_model=FactorExposure)
def upload_returns(file: UploadFile = File(...), asset_name: str = Form("Asset")):
    """上传资产月度收益 CSV（两列：date, return）并返回五因子暴露"""
    try:
        content = file.file.read()
        df = pd.read_csv(io.StringIO(content.decode("utf-8")))
    except Exception as e:
        raise HTTPException(status_code=400, detail="CSV 解析失败：" + str(e))

    if not {"date", "return"}.issubset(df.columns):
        raise HTTPException(status_code=400, detail="CSV 必须包含 date 和 return 列")

    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date").sort_index()
    df["return"] = pd.to_numeric(df["return"], errors="coerce") / 100  # 转小数

    # 合并因子数据
    merged = FACTORS.join(df, how="inner").dropna()
    if merged.empty or len(merged) < 36:
        raise HTTPException(
            status_code=400, detail="数据不足（需 ≥36 个月重叠样本）"
        )

    y = merged["return"]
    X = sm.add_constant(
        merged[["Mkt-RF", "SMB", "HML", "RMW", "CMA"]]
    )  # 市场超额收益 + 四因子
    model = sm.OLS(y, X).fit()

    return FactorExposure(
        alpha=float(model.params["const"]),
        beta_mkt=float(model.params["Mkt-RF"]),
        beta_smb=float(model.params["SMB"]),
        beta_hml=float(model.params["HML"]),
        beta_rmw=float(model.params["RMW"]),
        beta_cma=float(model.params["CMA"]),
        r_squared=float(model.rsquared),
        std_resid=float(model.resid.std()),
    )


@app.post("/forecast", response_model=Forecast)
def forecast(exposure: FactorExposure, horizon: int = Form(1)):
    """基于最新因子收益均值与暴露预测下月收益 + 1 个月 95% VaR"""
    latest = FACTORS.iloc[-horizon:].mean()
    pred_ret = (
        exposure.alpha
        + exposure.beta_mkt * latest["Mkt-RF"]
        + exposure.beta_smb * latest["SMB"]
        + exposure.beta_hml * latest["HML"]
        + exposure.beta_rmw * latest["RMW"]
        + exposure.beta_cma * latest["CMA"]
    )

    # 历史模拟 VaR：用残差标准差 + 正态假设
    var_95 = 1.65 * exposure.std_resid

    return Forecast(next_month_return=pred_ret, var_95=var_95)


@app.get("/export")
def export_report():
    """导出示例 PDF（占位，返回 CSV 模板）"""
    tpl = pd.DataFrame(
        {
            "Factor": ["Alpha", "MKT", "SMB", "HML", "RMW", "CMA"],
            "Exposure": [0.002, 1.1, 0.3, -0.2, 0.15, -0.05],
        }
    )
    buf = io.BytesIO()
    tpl.to_csv(buf, index=False)
    buf.seek(0)
    return StreamingResponse(
        buf,
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=ff5_template.csv"},
    )


# 健康检查
@app.get("/ping")
def ping():
    return {"status": "ok"}


# 本地开发入口
if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
